目录17

文章Matplotlib第 35 章

rcParams 与常用键

Matplotlib · 第 35 / 43 章

样式分三层,优先级从低到高:内置默认 → rcParams / .mplstyle → 函数参数。前两章调的都是第三层(plot(lw=2) 只管这一条线),这一章管中间那层——一处写完,整张图、整个脚本、整个项目都跟着变。

rcParams 不是一个配置文件,它是运行时的一个字典:matplotlib.rcParams['font.size'] = 12 改完之后,后创建的每一个 Text、Line2D 都会按 12 来实例化。意识到“改的是模板、不是已存在的对象”,这一章大部分坑就都不会踩。

三条写入路径:rcParams / rc / rc_context

Python7 行
import matplotlib as mpl

mpl.rcParams['font.size'] = 12                        # ① 逐键赋值,全局生效
mpl.rc('lines', linewidth=2, marker='s')               # ② 按组简写:lines.* 一起写
with mpl.rc_context({'axes.grid': True}):              # ③ 临时生效,出栈自动还原
    ...
mpl.rcdefaults()                                       # ④ 恢复 matplotlib 内置默认
写法 一句话 影响范围
rcParams['a.b'] = v 直接改字典 立刻生效,此后所有新对象
mpl.rc('a', b=v) 省掉前缀的批量赋值 同 rcParams,只是少打字
mpl.rc_context({...}) 上下文管理器 只在这个 with 块里生效
mpl.rcdefaults() 全部还原成出厂值 全局,不会回到你的 matplotlibrc
mpl.rc_file_defaults() 还原成 matplotlibrc 里的值 全局
mpl.rc_file(path) 从文件覆盖 默认先重置再读,即“整套替换”

最小示例

Python14 行
import matplotlib as mpl
import matplotlib.pyplot as plt

print(len(mpl.rcParams))
# 输出: 322
print(mpl.rcParams['font.size'], mpl.rcParams['axes.titlesize'])
# 输出: 10.0 large

mpl.rcParams['font.size'] = 16
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
print(ax.title.get_fontsize(), ax.xaxis.label.get_fontsize())
# 输出: 19.2 16.0
plt.show()

font.size 是 10 时标题 12pt('large' = 1.2×);改成 16 之后标题跟着变成 19.2——因为 axes.titlesize 存的是相对名,不是数字。这一点下面单独一节讲。

len(rcParams) 是 322,即 3.10.7 一共暴露 322 个可调键。按前缀分:

Python12 行
import matplotlib as mpl

ks = list(mpl.rcParams)
for pre in ['figure.', 'axes.', 'lines.', 'font.', 'savefig.', 'xtick.', 'legend.']:
    print(pre, sum(k.startswith(pre) for k in ks))
# 输出: figure. 24
# 输出: axes. 33
# 输出: lines. 17
# 输出: font. 11
# 输出: savefig. 9
# 输出: xtick. 21
# 输出: legend. 20

rcParams['a.b'] = value:直接赋值

键名就是「组.名」,两段用点分开。值会被当场校验,写错类型立刻报错、不会拖到画图时才炸:

Python9 行
import matplotlib as mpl

mpl.rcParams['font.size'] = 14
print(mpl.rcParams['font.size'])
# 输出: 14.0     ← 给整数,存进去变成浮点

mpl.rcParams['axes.grid'] = True
print(mpl.rcParams['axes.grid'])
# 输出: True

写不存在的键则直接 KeyError——这也是判断某个键在不在当前版本最快的办法:

Python9 行
import matplotlib as mpl

print('legend.linewidth' in mpl.rcParams)
# 输出: False
try:
    mpl.rcParams['legend.linewidth']
except KeyError as e:
    print('KeyError', e)
# 输出: KeyError 'legend.linewidth'

legend.linewidth 在 3.10.7 里根本不存在(网上一堆“3.11 新增 legend.linewidth“的说法,本机实测没有这个键)。要改图例边框粗细只能拿到 legend 对象再 leg.get_frame().set_linewidth()。

mpl.rc():按组简写

rc(group, **kwargs) 把 group. 前缀省掉,一次写多个键:

Python9 行
import matplotlib as mpl

mpl.rc('lines', linewidth=3, marker='s')
print(mpl.rcParams['lines.linewidth'], mpl.rcParams['lines.marker'])
# 输出: 3.0 s

mpl.rc('font', size=14, family='serif')
print(mpl.rcParams['font.size'], mpl.rcParams['font.family'])
# 输出: 14.0 ['serif']     ← family 内部仍是列表

组名写错或键名写错都会报 KeyError,报错里带着拼错的全名:

Python7 行
import matplotlib as mpl

try:
    mpl.rc('lines', notakey=1)
except KeyError as e:
    print('KeyError', e)
# 输出: KeyError 'Unrecognized key "lines.notakey" for group "lines" and name "notakey"'

只能用它改已存在的键,不能注册自己的键。

mpl.rc_context():临时生效,出栈还原

想“这一段用样式 A、下一段用样式 B”,不要手动存旧值再改回来——rc_context 就是干这个的:

Python8 行
import matplotlib as mpl

base = mpl.rcParams['font.size']
with mpl.rc_context({'font.size': 20, 'axes.grid': True}):
    print(mpl.rcParams['font.size'], mpl.rcParams['axes.grid'])
# 输出: 20.0 True
print(mpl.rcParams['font.size'], mpl.rcParams['axes.grid'], base)
# 输出: 10.0 False 10.0     ← 出栈已还原

出栈发生在 with 块结束处,异常退出也会还原。这是它相对“手动赋值 + 手动还原”的唯一价值。

它只吃字典,不吃 style 名字。想叠一套 style 得用 plt.style.context():

Python13 行
import matplotlib as mpl
import matplotlib.pyplot as plt

try:
    with mpl.rc_context(['seaborn-v0_8-whitegrid']):
        pass
except ValueError as e:
    print('ValueError', e)
# 输出: ValueError too many values to unpack (expected 2)

with plt.style.context(['seaborn-v0_8-whitegrid', {'axes.grid': False}]):
    print(mpl.rcParams['axes.grid'], mpl.rcParams['grid.color'])
# 输出: False .8

报错信息 too many values to unpack (expected 2) 完全看不出问题在哪——它把列表当成了 (key, value) 二元组在拆。记住:rc_context 只收字典,style.context 才收 style。

mpl.rcdefaults() 与 mpl.rc_file_defaults():还原

Python6 行
import matplotlib as mpl

mpl.rcParams['font.size'] = 33
mpl.rcdefaults()
print(mpl.rcParams['font.size'], len(mpl.rcParams))
# 输出: 10.0 322
  • rcdefaults():还原成代码里写死的出厂值。
  • rc_file_defaults():重新读 matplotlibrc 文件(mpl.matplotlib_fname() 指的那个)。

本机这两个结果相同,因为 matplotlibrc 是原封不动的模板,没改过任何键:

Python8 行
import matplotlib as mpl

mpl.rcParams['font.size'] = 33
mpl.rc_file_defaults()
print(mpl.rcParams['font.size'])
# 输出: 10.0     ← 本机 matplotlibrc 没动过,所以和 rcdefaults 结果一样
print(mpl.matplotlib_fname().split('site-packages')[-1])
# 输出: \matplotlib\mpl-data\matplotlibrc

但如果你把 font.size: 13 写进了 mpl.matplotlib_fname() 那个文件,rc_file_defaults() 会得到 13、rcdefaults() 仍是 10.0。区别只在“有没有改过全局 rc 文件”。

mpl.get_configdir() 是用户级配置目录(C:\Users\wx\.matplotlib),把 matplotlibrc 放这儿也能生效,优先级高于 site-packages 里的模板。

mpl.rc_file() / rc_params_from_file():从文件读

注意 rc_file 有个 use_default_template 参数,默认 True,含义是先按 matplotlibrc 模板重置一遍,再应用文件里的键:

Python11 行
import matplotlib as mpl
from pathlib import Path

Path('X_myrc.txt').write_text(
    'font.size: 15\naxes.grid: True\nlines.linewidth: 4\n', encoding='utf-8')

mpl.rcParams['axes.facecolor'] = '#ffeeee'
mpl.rc_file('X_myrc.txt')                      # 默认 use_default_template=True
print(mpl.rcParams['font.size'], mpl.rcParams['axes.grid'],
      mpl.rcParams['axes.facecolor'])
# 输出: 15.0 True white     ← facecolor 被重置回 white,我设的 #ffeeee 丢了

所以默认行为是“整套替换”:读到的是「模板 + 文件里的 3 个键」,你自己之前改的东西一律不在。

use_default_template=False 才是“只覆盖文件里出现的键”:

Python10 行
import matplotlib as mpl
from pathlib import Path

Path('X_myrc.txt').write_text(
    'font.size: 15\naxes.grid: True\nlines.linewidth: 4\n', encoding='utf-8')

mpl.rcParams['axes.facecolor'] = '#ffeeee'
mpl.rc_file('X_myrc.txt', use_default_template=False)
print(mpl.rcParams['font.size'], mpl.rcParams['axes.facecolor'])
# 输出: 15.0 #ffeeee        ← 只改了 font.size,facecolor 保留

rc_params_from_file() 更纯:不写 rcParams,返回一个 RcParams 让你自己决定怎么用:

Python9 行
import matplotlib as mpl
from pathlib import Path

Path('X_myrc.txt').write_text(
    'font.size: 15\naxes.grid: True\nlines.linewidth: 4\n', encoding='utf-8')

d = mpl.rc_params_from_file('X_myrc.txt', use_default_template=False)
print(type(d).__name__, len(d), d['font.size'])
# 输出: RcParams 3 15.0     ← 只解析出文件里那 3 个键

注意事项

  • rcParams 是 dict 的子类(RcParams),键按字母序存,不要依赖插入顺序。
  • 改 rcParams 只影响之后创建的对象。已经建好的 fig/ax/Text 不会回头改(见下一节的字号例子)。
  • 写错键 → KeyError;写错值 → ValueError(后者信息更友好,会把合法值列出来)。
  • rc_context 只收字典;要叠 style 用 plt.style.context([...])。
  • rcdefaults() 还原的是出厂值,不是你的 matplotlibrc;要回到文件值用 rc_file_defaults()。
  • 脚本里别在模块顶层改 rcParams——import 这个模块的代码会一起被改。用 rc_context 或 plt.style.context 圈住作用域。

综合示例:一套“论文出图”预设

把常用项写成字典,用 rc_context 圈住整张图。三格分别是折线、柱、散点,全部吃同一份预设:

Python46 行
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

PAPER = {
    'font.size': 9,
    'font.family': 'sans-serif',
    'font.sans-serif': ['Microsoft YaHei', 'DejaVu Sans'],
    'axes.titlesize': 'medium',
    'axes.titleweight': 'bold',
    'axes.titlepad': 6,
    'axes.grid': True,
    'axes.grid.axis': 'y',
    'grid.color': '#e3e3e3',
    'grid.linewidth': 0.6,
    'axes.spines.top': False,
    'axes.spines.right': False,
    'axes.linewidth': 0.8,
    'lines.linewidth': 1.2,
    'legend.frameon': False,
    'figure.dpi': 110,
    'savefig.dpi': 300,
}

x = np.linspace(0, 2 * np.pi, 60)

with mpl.rc_context(PAPER):
    fig, axs = plt.subplots(1, 3, figsize=(9, 2.8))

    axs[0].plot(x, np.sin(x), label='sin')
    axs[0].plot(x, np.cos(x), label='cos')
    axs[0].set_title('折线')
    axs[0].legend()

    axs[1].bar(['甲', '乙', '丙', '丁'], [12, 18, 9, 15])
    axs[1].set_title('柱状')

    rng = np.random.default_rng(7)
    axs[2].scatter(rng.normal(0, 1, 120), rng.normal(0, 1, 120), s=14)
    axs[2].set_title('散点')

    fig.tight_layout()
    plt.show()

print(round(mpl.rcParams['lines.linewidth'], 1))
# 输出: 1.5     ← 出栈后回到默认

savefig.dpi: 300 只对 savefig 生效,屏幕显示仍是 figure.dpi: 110——两者本来就分离,见「保存与导出」一章。

常用键与它们的真实默认值

网上流传的默认值不少是过时的。下面这批全部是 3.10.7 实测值:

分组 键 默认 一句话
画布 figure.figsize [6.4, 4.8] 英寸,不是像素
画布 figure.dpi 100.0 屏幕显示精度
画布 figure.facecolor 'white' 画布底色
画布 figure.autolayout False 旧的自动排版开关
画布 figure.constrained_layout.use False 新的自动排版开关
保存 savefig.dpi 'figure' 跟随 figure.dpi,不是固定 300
保存 savefig.bbox None 不裁白边
保存 savefig.pad_inches 0.1 bbox='tight' 时的留白
保存 savefig.facecolor 'auto' 跟随画布底色
字体 font.size 10.0 字号的基准,其它是相对它
字体 font.family ['sans-serif'] 大类
字体 axes.unicode_minus True 用 U+2212,中文图要关
坐标 axes.titlesize 'large' 相对名,不是数字
坐标 axes.labelsize 'medium' 同上
坐标 axes.titleweight 'normal'
坐标 axes.titlepad 6.0 不是 10
坐标 axes.linewidth 0.8 轴线粗细
坐标 axes.grid False
坐标 axes.spines.top / .right True 默认四边框齐全
线条 lines.linewidth 1.5 不是 1.0,也不是 1.8
线条 lines.markersize 6.0
刻度 xtick.direction 'out'
刻度 xtick.major.size 3.5 不是 4
刻度 xtick.minor.visible False
图例 legend.frameon True 默认有框
图例 legend.loc 'best'
网格 grid.color '#b0b0b0'
数学 mathtext.fontset 'dejavusans'
数学 text.usetex False 开它要装 LaTeX

出厂默认 vs 改 10 个键

右图只动了 10 个键:font.size(10→11)、axes.titlesize、axes.titleweight、axes.grid、grid.color、grid.linewidth、axes.spines.top、axes.spines.right、lines.linewidth(1.5→2.2)、legend.frameon——视觉效果已经完全是另一套。

逐个核对默认值:

Python21 行
import matplotlib as mpl

for k in ['figure.figsize', 'figure.dpi', 'savefig.dpi', 'savefig.bbox',
          'font.size', 'axes.titlepad', 'axes.linewidth',
          'lines.linewidth', 'lines.markersize',
          'xtick.major.size', 'xtick.minor.visible',
          'legend.frameon', 'grid.color']:
    print(f'{k:24s} {mpl.rcParams[k]!r}')
# 输出: figure.figsize           [6.4, 4.8]
# 输出: figure.dpi               100.0
# 输出: savefig.dpi              'figure'
# 输出: savefig.bbox             None
# 输出: font.size                10.0
# 输出: axes.titlepad            6.0
# 输出: axes.linewidth           0.8
# 输出: lines.linewidth          1.5
# 输出: lines.markersize         6.0
# 输出: xtick.major.size         3.5
# 输出: xtick.minor.visible      False
# 输出: legend.frameon           True
# 输出: grid.color               '#b0b0b0'

savefig.dpi 的默认值是字符串 'figure',含义是“跟着 figure.dpi 走”。这决定了两件事:不传 dpi 时导出尺寸完全由 figure.dpi 决定;只要 figure.dpi 变了,导出尺寸跟着变——存图精度和屏显精度在默认状态下是同一个数:

Python12 行
import matplotlib.pyplot as plt
from matplotlib.image import imread

fig, ax = plt.subplots(figsize=(2, 1))
fig.savefig('X_dpi_a.png')
print(imread('X_dpi_a.png').shape)
# 输出: (100, 200, 4)     ← 200×100 像素 = 2 英寸 × 100 dpi

fig.savefig('X_dpi_b.png', dpi=200)
print(imread('X_dpi_b.png').shape)
# 输出: (200, 400, 4)     ← 给了 dpi,覆盖 'figure'
plt.close(fig)

要“屏上看 100dpi、投稿 300dpi”,就是 figure.dpi: 100 + savefig.dpi: 300 两行,不要只改后者。

注意事项

  • axes.titlepad 默认 6.0(旧资料写 10)、lines.linewidth 默认 1.5(旧资料写 1.8)、xtick.major.size 默认 3.5(旧资料写 4)。
  • savefig.dpi 是 'figure' 而不是 300;savefig.bbox 是 None 而不是 'tight'。要裁白边得显式给。
  • legend.linewidth 在 3.10.7 不存在。改图例边框用 leg.get_frame().set_linewidth()。
  • mathtext.default 默认 'it';'normal' 不是合法值(下一节验证)。
  • axes.spines.top/.right 默认是 True——“去边框”是常见需求,所以到处都在改它,但它不是默认。

字号是相对的:font.size 与 axes.titlesize='large'

font.size 是基准,其余十几个 *.size 键都是相对名:xx-small 到 xx-large 七档,各乘一个固定倍率。

相对字号名换算成 pt

七档相对名的倍率

Python12 行
import matplotlib as mpl
from matplotlib.font_manager import FontProperties

for name in ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large']:
    print(f'{name:9s} {FontProperties(size=name).get_size_in_points():.2f} pt')
# 输出: xx-small  5.79 pt
# 输出: x-small   6.94 pt
# 输出: small     8.33 pt
# 输出: medium    10.00 pt
# 输出: large     12.00 pt
# 输出: x-large   14.40 pt
# 输出: xx-large  17.28 pt

换算关系是 font.size × {0.579, 0.694, 0.833, 1.0, 1.2, 1.44, 1.728}。记住 large = 1.2×、x-large = 1.44×、xx-large = 1.728× 就够用了——它不是等差,是 1.2 的幂。

存的是名字,不是数字

Python6 行
import matplotlib as mpl

print(mpl.rcParams['axes.titlesize'], mpl.rcParams['axes.labelsize'])
# 输出: large medium
print(mpl.rcParams['xtick.labelsize'], mpl.rcParams['legend.fontsize'])
# 输出: medium medium

所以在 rcParams 里写 axes.titlesize: 12 和写 axes.titlesize: large 是两种不同的语义:前者钉死 12pt,后者随 font.size 缩放。做主题时几乎总该用相对名,只有“标题就必须 12pt,别管正文多大”时才用数字。

改 font.size 不影响已经建好的 Axes

这是最容易踩的一个坑。字号在 Text 对象创建的那一刻就从 rcParams 解析成了具体 pt,之后再改 font.size 不会回头改它:

Python15 行
import matplotlib as mpl
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2])
print(round(ax.title.get_fontsize(), 1))
# 输出: 12.0          ← font.size=10 时 'large' = 12pt

mpl.rcParams['font.size'] = 20
print(round(ax.title.get_fontsize(), 1))
# 输出: 12.0          ← 老 ax 纹丝不动

fig2, ax2 = plt.subplots()
print(round(ax2.title.get_fontsize(), 1))
# 输出: 24.0          ← 新建的 ax2 才吃到 20

推论:改 rcParams 必须放在 plt.subplots() 之前。脚本里常见“先建图、后面想起来改字号、结果没生效”,就是因为顺序反了。

反过来说,ax.set_title(..., fontsize=14) 这种显式参数永远赢 rcParams,而且两者可以混用:rcParams 定大局,个别元素单独覆盖。

mathtext.default 没有 'normal'

旧资料里有“3.11 起 mathtext.default 区分 'normal' 和 'italic',老配置要改成 'normal'“的说法。实测 3.10.7 里 'normal' 直接被拒:

Python9 行
import matplotlib as mpl

print(mpl.rcParams['mathtext.default'])
# 输出: it
try:
    mpl.rcParams['mathtext.default'] = 'normal'
except ValueError as e:
    print('ValueError', str(e)[:96])
# 输出: ValueError Key mathtext.default: 'normal' is not a valid value for mathtext.default; supported values are [

合法值是 rm / cal / bfit / it / tt / sf / bf / default / bb / frak / scr / regular / ...,没有 normal。要让公式里的字母直立,写 'regular' 或 'rm':

Python8 行
import matplotlib as mpl

for v in ['it', 'regular', 'rm']:
    mpl.rcParams['mathtext.default'] = v
    print(v, '→', mpl.rcParams['mathtext.default'])
# 输出: it → it
# 输出: regular → regular
# 输出: rm → rm

注意事项

  • font.size 是基准,其余 *.size 默认都是相对名;改基准等于整体缩放。
  • 相对字号是 1.2 的幂,不是等差:small 8.33 / medium 10 / large 12 的间距并不相等。
  • 改 rcParams 要在建 Figure 之前;已建对象只能逐个 set_fontsize()。
  • 显式参数(fontsize=14)永远优先于 rcParams。
  • mathtext.default='normal' 会抛 ValueError,用 'regular' 或 'rm'。
  • 中文图要配 font.sans-serif 一起用,并且把 axes.unicode_minus 设为 False——否则负号会变成方框。